package org.stagemonitor.core.instrument;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * <p>
 * A thread-safe map with weak keys. Entries are based on a key's system hash code and keys are considered
 * equal only by reference equality.
 * </p>
 * This class does not implement the {@link java.util.Map} interface because this implementation is incompatible
 * with the map contract.
 */
public class WeakConcurrentMap<K, V> extends ReferenceQueue<K> implements Runnable {

	private static final AtomicLong ID = new AtomicLong();

	protected final ConcurrentMap<WeakKey<K>, V> target;

	private final Thread thread;

	/**
	 * @param cleanerThread {@code true} if a thread should be started that removes stale entries.
	 */
	public WeakConcurrentMap(boolean cleanerThread) {
		target = new ConcurrentHashMap<WeakKey<K>, V>();
		if (cleanerThread) {
			thread = new Thread(this);
			thread.setName("weak-ref-cleaner-" + ID.getAndIncrement());
			thread.setPriority(Thread.MIN_PRIORITY);
			thread.setDaemon(true);
			thread.start();
		} else {
			thread = null;
		}
	}

	/**
	 * @param key The key of the entry.
	 * @return The value of the entry or the default value if it did not exist.
	 */
	public V get(K key) {
		if (key == null) throw new NullPointerException();
		V value = target.get(new WeakKey<K>(key));
		if (value == null) {
			value = defaultValue(key);
			if (value != null) {
				V previousValue = target.putIfAbsent(new WeakKey<K>(key, this), value);
				if (previousValue != null) {
					value = previousValue;
				}
			}
		}
		return value;
	}

	/**
	 * @param key The key of the entry.
	 * @return {@code true} if the key already defines a value.
	 */
	public boolean containsKey(K key) {
		return target.containsKey(new WeakKey<K>(key));
	}

	/**
	 * @param key The key of the entry.
	 * @param value The value of the entry.
	 * @return The previous entry or {@code null} if it does not exist.
	 */
	public V put(K key, V value) {
		if (key == null || value == null) throw new NullPointerException();
		return target.put(new WeakKey<K>(key, this), value);
	}

	/**
	 * @param key The key of the entry.
	 * @param value The value of the entry.
	 * @return The previous entry or {@code null} if it does not exist.
	 */
	public V putIfAbsent(K key, V value) {
		if (key == null || value == null) throw new NullPointerException();
		return target.putIfAbsent(new WeakKey<K>(key, this), value);
	}

	/**
	 * @param key The key of the entry.
	 * @return The removed entry or {@code null} if it does not exist.
	 */
	public V remove(K key) {
		if (key == null) throw new NullPointerException();
		return target.remove(new WeakKey<K>(key));
	}

	/**
	 * Clears the entire map.
	 */
	public void clear() {
		target.clear();
	}

	/**
	 * Creates a default value. There is no guarantee that the requested value will be set as a once it is created
	 * in case that another thread requests a value for a key concurrently.
	 *
	 * @param key The key for which to create a default value.
	 * @return The default value for a key without value.
	 */
	protected V defaultValue(K key) {
		return null;
	}

	/**
	 * @return The cleaner thread or {@code null} if no such thread was set.
	 */
	public Thread getCleanerThread() {
		return thread;
	}

	@Override
	public void run() {
		try {
			while (true) {
				target.remove(remove());
			}
		} catch (InterruptedException ignored) {
			clear();
		}
	}

	/*
	 * Why this works:
	 * ---------------
	 *
	 * Note that this map only supports reference equality for keys and uses system hash codes. Also, for the
	 * WeakKey instances to function correctly, we are voluntarily breaking the Java API contract for
	 * hashCode/equals of these instances.
	 *
	 *
	 * System hash codes are immutable and can therefore be computed prematurely and are stored explicitly
	 * within the WeakKey instances. This way, we always know the correct hash code of a key and always
	 * end up in the correct bucket of our target map. This remains true even after the weakly referenced
	 * key is collected.
	 *
	 * If we are looking up the value of the current key via WeakConcurrentMap::get or any other public
	 * API method, we know that any value associated with this key must still be in the map as the mere
	 * existence of this key makes it ineligible for garbage collection. Therefore, looking up a value
	 * using another WeakKey wrapper guarantees a correct result.
	 *
	 * If we are looking up the map entry of a WeakKey after polling it from the reference queue, we know
	 * that the actual key was already collected and calling WeakKey::get returns null for both the polled
	 * instance and the instance within the map. Since we explicitly stored the identity hash code for the
	 * referenced value, it is however trivial to identify the correct bucket. From this bucket, the first
	 * weak key with a null reference is removed. Due to hash collision, we do not know if this entry
	 * represents the weak key. However, we do know that the reference queue polls at least as many weak
	 * keys as there are stale map entries within the target map. If no key is ever removed from the map
	 * explicitly, the reference queue eventually polls exactly as many weak keys as there are stale entries.
	 *
	 * Therefore, we can guarantee that there is no memory leak.
	 */
	private static class WeakKey<T> extends WeakReference<T> {

		private final int hashCode;

		WeakKey(T key) {
			super(key);
			hashCode = System.identityHashCode(key);
		}

		WeakKey(T key, ReferenceQueue<? super T> queue) {
			super(key, queue);
			hashCode = System.identityHashCode(key);
		}

		@Override
		public int hashCode() {
			return hashCode;
		}

		@Override
		public boolean equals(Object other) {
			return ((WeakKey<?>) other).get() == get();
		}
	}

	/**
	 * A {@link WeakConcurrentMap} where stale entries are removed as a side effect of interacting with this map.
	 */
	public static class WithInlinedExpunction<K, V> extends WeakConcurrentMap<K, V> {

		public WithInlinedExpunction() {
			super(false);
		}

		@Override
		public V get(K key) {
			expungeStaleEntries();
			return super.get(key);
		}

		@Override
		public boolean containsKey(K key) {
			expungeStaleEntries();
			return super.containsKey(key);
		}

		@Override
		public V put(K key, V value) {
			expungeStaleEntries();
			return super.put(key, value);
		}

		@Override
		public V putIfAbsent(K key, V value) {
			expungeStaleEntries();
			return super.put(key, value);
		}

		@Override
		public V remove(K key) {
			expungeStaleEntries();
			return super.remove(key);
		}

		void expungeStaleEntries() {
			Reference<?> reference;
			while ((reference = poll()) != null) {
				target.remove(reference);
			}
		}
	}
}